﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using Microsoft.Synchronization;
using NUnit.Framework;
using NMock2;
using NMock2.Actions;
using Obra.Sync;
using Obra.Common;

namespace Obra.Test
{
    [TestFixture]
    public class SyncTester
        : NMockTester
    {
        private string syncMetadataFileNameA = "syncFileA";
        private string syncMetadataFileNameB = "syncFileB";

        public override void SetUp()
        {
            base.SetUp();

            TestHelper.DeleteFile(this.syncMetadataFileNameA);
            TestHelper.DeleteFile(this.syncMetadataFileNameB);        
        }

        private void AssertAgentStatisticsHaveChanged(
            Orchestrator agent,
            int downloadChangesApplied,
            int downloadChangesFailed,
            int uploadChangesApplied,
            int uploadChangesFailed)
        {
            int downloadTotal = downloadChangesApplied + downloadChangesFailed;

            Assert.AreEqual(downloadChangesApplied, agent.Statistics.DownloadChangesApplied);
            Assert.AreEqual(downloadChangesFailed, agent.Statistics.DownloadChangesFailed);
            Assert.AreEqual(downloadTotal, agent.Statistics.DownloadChangesTotal);

            int uploadTotal = uploadChangesApplied + uploadChangesFailed;

            Assert.AreEqual(uploadChangesApplied, agent.Statistics.UploadChangesApplied);
            Assert.AreEqual(uploadChangesFailed, agent.Statistics.UploadChangesFailed);
            Assert.AreEqual(uploadTotal, agent.Statistics.UploadChangesTotal);
        }

        private void AssertAgentStatisticsUploadChangesApplied(
            Orchestrator agent,
            int uploadChangesApplied)
        {
            this.AssertAgentStatisticsHaveChanged(
                agent,
                0,
                0,
                uploadChangesApplied,
                0);
        }

        private void AssertNoAgentStatisticsHaveChanged(Orchestrator agent)
        {
            this.AssertAgentStatisticsHaveChanged(
                agent,
                0,
                0,
                0,
                0);
        }

        /// <summary>
        /// Assert that a synchronization run results in no downloads or uploads.
        /// </summary>
        /// <param name="items">
        private void AssertNoSynchronizationChanges(
            List<MediaItemMetadata> items)
        {
            ISyncService syncServiceA = this.Mockery.NewMock<ISyncService>();
            ISyncService syncServiceB = this.Mockery.NewMock<ISyncService>();

            using (this.Mockery.Ordered)
            {
                Expect.Once.On(syncServiceA).Method("Connect");
                Expect.Once.On(syncServiceA).Method("EnumerateMediaFiles").Will(Return.Value(items));

                Expect.Once.On(syncServiceB).Method("Connect");
                Expect.Once.On(syncServiceB).Method("EnumerateMediaFiles").Will(Return.Value(items));

                Expect.Once.On(syncServiceB).Method("Disconnect");
                Expect.Once.On(syncServiceA).Method("Disconnect");
            }

            Obra.Sync.SyncProvider syncProviderA = new Obra.Sync.SyncProvider(
                syncServiceA,
                this.syncMetadataFileNameA);

            Obra.Sync.SyncProvider syncProviderB = new Obra.Sync.SyncProvider(
                syncServiceB,
                this.syncMetadataFileNameB);

            using (Orchestrator agent = new Orchestrator(
                syncProviderA,
                syncProviderB))
            {
                agent.Upload();
                this.AssertNoAgentStatisticsHaveChanged(agent);
            }
        }

        /// <summary>
        /// Add the specified list of media items to the metadata store.
        /// </summary>
        /// <param name="items">
        /// List of media item metadata to add to the data store.
        /// </param>
        private void AssertAddItemsToMetadataStore(
            List<MediaItemMetadata> items)
        {
            ISyncService syncServiceA = this.Mockery.NewMock<ISyncService>();
            ISyncService syncServiceB = this.Mockery.NewMock<ISyncService>();

            using (this.Mockery.Ordered)
            {
                Expect.Once.On(syncServiceA).Method("Connect");
                Expect.Once.On(syncServiceA).Method("EnumerateMediaFiles").Will(Return.Value(items));

                foreach (var mediaItemMetadata in items)
                {
                    Expect.Once.On(syncServiceA).Method("ComputeChecksum")
                        .With(mediaItemMetadata)
                        .Will(Return.Value(mediaItemMetadata.Checksum));
                }

                Expect.Once.On(syncServiceB).Method("Connect");
                Expect.Once.On(syncServiceB).Method("EnumerateMediaFiles").Will(Return.Value(items));

                foreach (var mediaItemMetadata in items)
                {
                    Expect.Once.On(syncServiceB).Method("ComputeChecksum")
                        .With(mediaItemMetadata)
                        .Will(Return.Value(mediaItemMetadata.Checksum));
                }

                Expect.Once.On(syncServiceB).Method("Disconnect");
                Expect.Once.On(syncServiceA).Method("Disconnect");
            }

            Obra.Sync.SyncProvider syncProviderA = new Obra.Sync.SyncProvider(
                syncServiceA,
                this.syncMetadataFileNameA);

            Obra.Sync.SyncProvider syncProviderB = new Obra.Sync.SyncProvider(
                syncServiceB,
                this.syncMetadataFileNameB);

            using (Orchestrator agent = new Orchestrator(
                syncProviderA,
                syncProviderB))
            {
                agent.Upload();
                this.AssertAgentStatisticsUploadChangesApplied(agent, items.Count);
            }

            this.AssertNoSynchronizationChanges(
                items);
        }

        /// <summary>
        /// The local and remote sync providers have no files, so there is
        /// nothing to do.
        /// </summary>
        [Test]
        public void Sync_NoItems_Upload()
        {
            ISyncService syncServiceA = this.Mockery.NewMock<ISyncService>();
            ISyncService syncServiceB = this.Mockery.NewMock<ISyncService>();

            this.AssertNoSynchronizationChanges(new List<MediaItemMetadata>());
        }


        /// <summary>
        /// The local and remote item are identical.  This is the first time 
        /// these items have been added to the store.  The local sync provider
        /// will overwrite the remote provider's sync version, but no data will
        /// be exchanged.
        /// </summary>
        [Test]
        public void Sync_IdenticalItems_Upload()
        {
            MediaItemMetadata itemA = new MediaItemMetadata()
            {
                Name = "nameA",
                Checksum = "checksumA",
                LastModifiedUtc = 0,
            };

            List<MediaItemMetadata> items = new List<MediaItemMetadata>()
            {
                itemA,
            };

            ISyncService syncServiceA = this.Mockery.NewMock<ISyncService>();
            ISyncService syncServiceB = this.Mockery.NewMock<ISyncService>();

            ISyncItemTransfer syncItemTransferA = this.Mockery.NewMock<ISyncItemTransfer>();

            using (this.Mockery.Ordered)
            {
                Expect.Once.On(syncServiceA).Method("Connect");
                Expect.Once.On(syncServiceA).Method("EnumerateMediaFiles").Will(Return.Value(items));
                Expect.Once.On(syncServiceA).Method("ComputeChecksum")
                    .With(itemA)
                    .Will(Return.Value(itemA.Checksum));

                Expect.Once.On(syncServiceB).Method("Connect");
                Expect.Once.On(syncServiceB).Method("EnumerateMediaFiles").Will(Return.Value(items));
                Expect.Once.On(syncServiceB).Method("ComputeChecksum")
                    .With(itemA)
                    .Will(Return.Value(itemA.Checksum));

                Expect.Once.On(syncServiceA).Method("LoadChange")
                    .With(itemA.Name)
                    .Will(Return.Value(syncItemTransferA));

                Expect.Once.On(syncItemTransferA).GetProperty("MediaItemMetadata")
                    .Will(Return.Value(itemA));


                Expect.Once.On(syncServiceB).Method("Disconnect");
                Expect.Once.On(syncServiceA).Method("Disconnect");
            }

            Obra.Sync.SyncProvider syncProviderA = new Obra.Sync.SyncProvider(
                syncServiceA,
                this.syncMetadataFileNameA);

            Obra.Sync.SyncProvider syncProviderB = new Obra.Sync.SyncProvider(
                syncServiceB,
                this.syncMetadataFileNameB);

            using (Orchestrator agent = new Orchestrator(
                syncProviderA,
                syncProviderB))
            {
                agent.Upload();
                this.AssertAgentStatisticsUploadChangesApplied(agent, items.Count);
            }

            this.AssertNoSynchronizationChanges(
                items);
        }

        /// <summary>
        /// Local sync provider has one file, and the remote provider has none.
        /// The local file is uploaded to the remote provider.
        /// </summary>
        [Test]
        public void Sync_OneLocalItem_Upload()
        {
            MediaItemMetadata itemA = new MediaItemMetadata()
            {
                Name = "nameA",
                Checksum = "checksumA",
                LastModifiedUtc = 0,
            };
            
            ISyncItemTransfer syncItemTransferA = this.Mockery.NewMock<ISyncItemTransfer>();

            List<MediaItemMetadata> items = new List<MediaItemMetadata>()
            {
                itemA,
            };

            ISyncService syncServiceA = this.Mockery.NewMock<ISyncService>();
            ISyncService syncServiceB = this.Mockery.NewMock<ISyncService>();

            using (this.Mockery.Ordered)
            {
                Expect.Once.On(syncServiceA).Method("Connect");
                Expect.Once.On(syncServiceA).Method("EnumerateMediaFiles").Will(Return.Value(items));
                Expect.Once.On(syncServiceA).Method("ComputeChecksum").WithAnyArguments().Will(Return.Value(itemA.Checksum));

                Expect.Once.On(syncServiceB).Method("Connect");
                Expect.Once.On(syncServiceB).Method("EnumerateMediaFiles").Will(Return.Value(new List<MediaItemMetadata>()));

                Expect.Once.On(syncServiceA).Method("LoadChange")
                    .With("nameA")
                    .Will(Return.Value(syncItemTransferA));

                Expect.Once.On(syncServiceB).Method("SaveChange")
                    .WithAnyArguments();

                Expect.Once.On(syncItemTransferA).GetProperty("MediaItemMetadata")
                    .Will(Return.Value(itemA));

                Expect.Once.On(syncServiceB).Method("Disconnect");
                Expect.Once.On(syncServiceA).Method("Disconnect");
            }

            Obra.Sync.SyncProvider syncProviderA = new Obra.Sync.SyncProvider(
                syncServiceA,
                this.syncMetadataFileNameA);

            Obra.Sync.SyncProvider syncProviderB = new Obra.Sync.SyncProvider(
                syncServiceB,
                this.syncMetadataFileNameB);

            using (Orchestrator agent = new Orchestrator(
                syncProviderA,
                syncProviderB))
            {
                agent.Upload();
                this.AssertAgentStatisticsUploadChangesApplied(agent, 1);
            }

            this.AssertNoSynchronizationChanges(
                items);
        }

        /// <summary>
        /// The local item is newer than the remote item.  The timestamp of the
        /// local item is newer than the remote item, but the checksums are 
        /// idenitical.  No file should be uploaded, and no change should be
        /// recorded.
        /// </summary>
        [Test]
        public void Sync_OneNewerLocalItem_Upload()
        {
            MediaItemMetadata itemA = new MediaItemMetadata()
            {
                Name = "nameA",
                Checksum = "checksumA",
                LastModifiedUtc = 0,
            };

            List<MediaItemMetadata> items = new List<MediaItemMetadata>()
            {
                itemA,
            };
            
            ISyncService syncServiceA = this.Mockery.NewMock<ISyncService>();
            ISyncService syncServiceB = this.Mockery.NewMock<ISyncService>();

            ISyncItemTransfer syncItemTransferA = this.Mockery.NewMock<ISyncItemTransfer>();

            using (this.Mockery.Ordered)
            {
                Expect.Once.On(syncServiceA).Method("Connect");
                Expect.Once.On(syncServiceA).Method("EnumerateMediaFiles").Will(Return.Value(items));
                Expect.Once.On(syncServiceA).Method("ComputeChecksum")
                    .With(itemA)
                    .Will(Return.Value(itemA.Checksum));

                Expect.Once.On(syncServiceB).Method("Connect");
                Expect.Once.On(syncServiceB).Method("EnumerateMediaFiles").Will(Return.Value(items));
                Expect.Once.On(syncServiceB).Method("ComputeChecksum")
                    .With(itemA)
                    .Will(Return.Value(itemA.Checksum));

                Expect.Once.On(syncServiceA).Method("LoadChange")
                    .With(itemA.Name)
                    .Will(Return.Value(syncItemTransferA));

                Expect.Once.On(syncItemTransferA).GetProperty("MediaItemMetadata")
                    .Will(Return.Value(itemA));


                Expect.Once.On(syncServiceB).Method("Disconnect");
                Expect.Once.On(syncServiceA).Method("Disconnect");
            }

            Obra.Sync.SyncProvider syncProviderA = new Obra.Sync.SyncProvider(
                syncServiceA,
                this.syncMetadataFileNameA);

            Obra.Sync.SyncProvider syncProviderB = new Obra.Sync.SyncProvider(
                syncServiceB,
                this.syncMetadataFileNameB);

            using (Orchestrator agent = new Orchestrator(
                syncProviderA,
                syncProviderB))
            {
                agent.Upload();
                this.AssertAgentStatisticsUploadChangesApplied(agent, items.Count);
            }

            this.AssertNoSynchronizationChanges(
                items);

            // update the local itemA

            MediaItemMetadata newItemA = new MediaItemMetadata()
            {
                Name = "nameA",
                Checksum = "checksumA",
                LastModifiedUtc = 1,
            };

            List<MediaItemMetadata> newItems = new List<MediaItemMetadata>()
            {
                newItemA,
            };

            syncServiceA = this.Mockery.NewMock<ISyncService>();
            syncServiceB = this.Mockery.NewMock<ISyncService>();

            syncItemTransferA = this.Mockery.NewMock<ISyncItemTransfer>();

            using (this.Mockery.Ordered)
            {
                Expect.Once.On(syncServiceA).Method("Connect");
                Expect.Once.On(syncServiceA).Method("EnumerateMediaFiles").Will(Return.Value(newItems));
                Expect.Once.On(syncServiceA).Method("ComputeChecksum").WithAnyArguments().Will(Return.Value(newItemA.Checksum));

                Expect.Once.On(syncServiceB).Method("Connect");
                Expect.Once.On(syncServiceB).Method("EnumerateMediaFiles").Will(Return.Value(items));

                Expect.Once.On(syncServiceA).Method("LoadChange")
                    .With(itemA.Name)
                    .Will(Return.Value(syncItemTransferA));

                Expect.Once.On(syncItemTransferA).GetProperty("MediaItemMetadata")
                    .Will(Return.Value(newItemA));

                Expect.Once.On(syncServiceB).Method("Disconnect");
                Expect.Once.On(syncServiceA).Method("Disconnect");
            }

            syncProviderA = new Obra.Sync.SyncProvider(
                syncServiceA,
                this.syncMetadataFileNameA);

            syncProviderB = new Obra.Sync.SyncProvider(
                syncServiceB,
                this.syncMetadataFileNameB);

            using (Orchestrator agent = new Orchestrator(
                syncProviderA,
                syncProviderB))
            {
                agent.Upload();
                this.AssertAgentStatisticsUploadChangesApplied(agent, 1);
            }

            this.AssertNoSynchronizationChanges(
                newItems);
        }

        /// <summary>
        /// Local and remote sync provider start off with identical files.  The 
        /// local sync provider's file is deleted, and should be deleted from 
        /// the remote provider.
        /// </summary>
        [Test]
        public void Sync_DeleteLocalItem_Upload()
        {
            MediaItemMetadata itemA = new MediaItemMetadata()
            {
                Name = "nameA",
                Checksum = "checksumA",
                LastModifiedUtc = 0,
            };

            List<MediaItemMetadata> items = new List<MediaItemMetadata>()
            {
                itemA,
            };

            ISyncService syncServiceA = this.Mockery.NewMock<ISyncService>();
            ISyncService syncServiceB = this.Mockery.NewMock<ISyncService>();

            ISyncItemTransfer syncItemTransferA = this.Mockery.NewMock<ISyncItemTransfer>();

            using (this.Mockery.Ordered)
            {
                Expect.Once.On(syncServiceA).Method("Connect");
                Expect.Once.On(syncServiceA).Method("EnumerateMediaFiles").Will(Return.Value(items));
                Expect.Once.On(syncServiceA).Method("ComputeChecksum")
                    .With(itemA)
                    .Will(Return.Value(itemA.Checksum));

                Expect.Once.On(syncServiceB).Method("Connect");
                Expect.Once.On(syncServiceB).Method("EnumerateMediaFiles").Will(Return.Value(items));
                Expect.Once.On(syncServiceB).Method("ComputeChecksum")
                    .With(itemA)
                    .Will(Return.Value(itemA.Checksum));

                Expect.Once.On(syncServiceA).Method("LoadChange")
                    .With(itemA.Name)
                    .Will(Return.Value(syncItemTransferA));

                Expect.Once.On(syncItemTransferA).GetProperty("MediaItemMetadata")
                    .Will(Return.Value(itemA));


                Expect.Once.On(syncServiceB).Method("Disconnect");
                Expect.Once.On(syncServiceA).Method("Disconnect");
            }

            Obra.Sync.SyncProvider syncProviderA = new Obra.Sync.SyncProvider(
                syncServiceA,
                this.syncMetadataFileNameA);

            Obra.Sync.SyncProvider syncProviderB = new Obra.Sync.SyncProvider(
                syncServiceB,
                this.syncMetadataFileNameB);

            using (Orchestrator agent = new Orchestrator(
                syncProviderA,
                syncProviderB))
            {
                agent.Upload();
                this.AssertAgentStatisticsUploadChangesApplied(agent, items.Count);
            }

            this.AssertNoSynchronizationChanges(
                items);

            // delete the item from the local provider (A)

            syncServiceA = this.Mockery.NewMock<ISyncService>();
            syncServiceB = this.Mockery.NewMock<ISyncService>();

            syncItemTransferA = this.Mockery.NewMock<ISyncItemTransfer>();

            CollectAction deleteMediaFileCollect = new CollectAction(0);

            using (this.Mockery.Ordered)
            {
                Expect.Once.On(syncServiceA).Method("Connect");
                Expect.Once.On(syncServiceA).Method("EnumerateMediaFiles").Will(Return.Value(new List<MediaItemMetadata>()));

                Expect.Once.On(syncServiceB).Method("Connect");
                Expect.Once.On(syncServiceB).Method("EnumerateMediaFiles").Will(Return.Value(items));


                Expect.Once.On(syncServiceB).Method("DeleteMediaFile")
                    .WithAnyArguments()
                    .Will(deleteMediaFileCollect);

                Expect.Once.On(syncServiceB).Method("Disconnect");
                Expect.Once.On(syncServiceA).Method("Disconnect");
            }

            syncProviderA = new Obra.Sync.SyncProvider(
                syncServiceA,
                this.syncMetadataFileNameA);

            syncProviderB = new Obra.Sync.SyncProvider(
                syncServiceB,
                this.syncMetadataFileNameB);

            using (Orchestrator agent = new Orchestrator(
                syncProviderA,
                syncProviderB))
            {
                agent.Upload();
                this.AssertAgentStatisticsUploadChangesApplied(agent, 1);
            }

            MediaItemMetadata mediaItemMetadata = (MediaItemMetadata)deleteMediaFileCollect.Parameter;
            
            Assert.AreEqual("nameA", mediaItemMetadata.Name);
            Assert.AreEqual("checksumA", mediaItemMetadata.Checksum);
            Assert.AreEqual(0, mediaItemMetadata.LastModifiedUtc);

            this.AssertNoSynchronizationChanges(
                new List<MediaItemMetadata>());
        }

        /// <summary>
        /// Local and remote sync provider start off with identical files.  The 
        /// remote sync provider's file is deleted, and is marked as deleted.
        /// </summary>
        /// <remarks>
        /// <para>
        /// The file will not be re-uploaded from the local provider to the 
        /// remote provider.  This is the way the Sync Framework works.
        /// </para>
        /// </remarks>
        [Test]
        public void Sync_DeleteRemoteItem_Upload()
        {
            MediaItemMetadata itemA = new MediaItemMetadata()
            {
                Name = "nameA",
                Checksum = "checksumA",
                LastModifiedUtc = 0,
            };

            List<MediaItemMetadata> items = new List<MediaItemMetadata>()
            {
                itemA,
            };

            ISyncService syncServiceA = this.Mockery.NewMock<ISyncService>();
            ISyncService syncServiceB = this.Mockery.NewMock<ISyncService>();

            ISyncItemTransfer syncItemTransferA = this.Mockery.NewMock<ISyncItemTransfer>();

            using (this.Mockery.Ordered)
            {
                Expect.Once.On(syncServiceA).Method("Connect");
                Expect.Once.On(syncServiceA).Method("EnumerateMediaFiles").Will(Return.Value(items));
                Expect.Once.On(syncServiceA).Method("ComputeChecksum")
                    .With(itemA)
                    .Will(Return.Value(itemA.Checksum));

                Expect.Once.On(syncServiceB).Method("Connect");
                Expect.Once.On(syncServiceB).Method("EnumerateMediaFiles").Will(Return.Value(items));
                Expect.Once.On(syncServiceB).Method("ComputeChecksum")
                    .With(itemA)
                    .Will(Return.Value(itemA.Checksum));

                Expect.Once.On(syncServiceA).Method("LoadChange")
                    .With(itemA.Name)
                    .Will(Return.Value(syncItemTransferA));

                Expect.Once.On(syncItemTransferA).GetProperty("MediaItemMetadata")
                    .Will(Return.Value(itemA));

                Expect.Once.On(syncServiceB).Method("Disconnect");
                Expect.Once.On(syncServiceA).Method("Disconnect");
            }

            Obra.Sync.SyncProvider syncProviderA = new Obra.Sync.SyncProvider(
                syncServiceA,
                this.syncMetadataFileNameA);

            Obra.Sync.SyncProvider syncProviderB = new Obra.Sync.SyncProvider(
                syncServiceB,
                this.syncMetadataFileNameB);

            using (Orchestrator agent = new Orchestrator(
                syncProviderA,
                syncProviderB))
            {
                agent.Upload();
                this.AssertAgentStatisticsUploadChangesApplied(agent, items.Count);
            }

            this.AssertNoSynchronizationChanges(
                items);

            // delete the item from the remote provider (B)

            syncServiceA = this.Mockery.NewMock<ISyncService>();
            syncServiceB = this.Mockery.NewMock<ISyncService>();

            syncItemTransferA = this.Mockery.NewMock<ISyncItemTransfer>();

            using (this.Mockery.Ordered)
            {
                Expect.Once.On(syncServiceA).Method("Connect");
                Expect.Once.On(syncServiceA).Method("EnumerateMediaFiles").Will(Return.Value(items));

                Expect.Once.On(syncServiceB).Method("Connect");
                Expect.Once.On(syncServiceB).Method("EnumerateMediaFiles").Will(Return.Value(new List<MediaItemMetadata>()));

                Expect.Once.On(syncServiceB).Method("Disconnect");
                Expect.Once.On(syncServiceA).Method("Disconnect");
            }

            syncProviderA = new Obra.Sync.SyncProvider(
                syncServiceA,
                this.syncMetadataFileNameA);

            syncProviderB = new Obra.Sync.SyncProvider(
                syncServiceB,
                this.syncMetadataFileNameB);

            using (Orchestrator agent = new Orchestrator(
                syncProviderA,
                syncProviderB))
            {
                agent.Upload();
                this.AssertAgentStatisticsUploadChangesApplied(agent, 0);
            }
        }


        /// <summary>
        /// Local and remote sync provider start off with identical files.  The 
        /// local sync provider's file is deleted, and should be deleted from 
        /// the remote provider.  The local provider adds a new file with the
        /// same name as the file it just deleted.
        /// 
        /// The file is not in the metadata store, because tombstones are cleaned
        /// up before every synchronization session.
        /// </summary>
        [Test]
        public void Sync_ResurrectLocalItem_Upload()
        {
            MediaItemMetadata itemA = new MediaItemMetadata()
            {
                Name = "nameA",
                Checksum = "checksumA",
                LastModifiedUtc = 0,
            };

            List<MediaItemMetadata> items = new List<MediaItemMetadata>()
            {
                itemA,
            };

            ISyncService syncServiceA = this.Mockery.NewMock<ISyncService>();
            ISyncService syncServiceB = this.Mockery.NewMock<ISyncService>();

            ISyncItemTransfer syncItemTransferA = this.Mockery.NewMock<ISyncItemTransfer>();

            using (this.Mockery.Ordered)
            {
                Expect.Once.On(syncServiceA).Method("Connect");
                Expect.Once.On(syncServiceA).Method("EnumerateMediaFiles").Will(Return.Value(items));
                Expect.Once.On(syncServiceA).Method("ComputeChecksum")
                    .With(itemA)
                    .Will(Return.Value(itemA.Checksum));

                Expect.Once.On(syncServiceB).Method("Connect");
                Expect.Once.On(syncServiceB).Method("EnumerateMediaFiles").Will(Return.Value(items));
                Expect.Once.On(syncServiceB).Method("ComputeChecksum")
                    .With(itemA)
                    .Will(Return.Value(itemA.Checksum));

                Expect.Once.On(syncServiceA).Method("LoadChange")
                    .With(itemA.Name)
                    .Will(Return.Value(syncItemTransferA));

                Expect.Once.On(syncItemTransferA).GetProperty("MediaItemMetadata")
                    .Will(Return.Value(itemA));


                Expect.Once.On(syncServiceB).Method("Disconnect");
                Expect.Once.On(syncServiceA).Method("Disconnect");
            }

            Obra.Sync.SyncProvider syncProviderA = new Obra.Sync.SyncProvider(
                syncServiceA,
                this.syncMetadataFileNameA);

            Obra.Sync.SyncProvider syncProviderB = new Obra.Sync.SyncProvider(
                syncServiceB,
                this.syncMetadataFileNameB);

            using (Orchestrator agent = new Orchestrator(
                syncProviderA,
                syncProviderB))
            {
                agent.Upload();
                this.AssertAgentStatisticsUploadChangesApplied(agent, items.Count);
            }

            this.AssertNoSynchronizationChanges(
                items);

            // delete the item from the local provider (A)

            syncServiceA = this.Mockery.NewMock<ISyncService>();
            syncServiceB = this.Mockery.NewMock<ISyncService>();

            syncItemTransferA = this.Mockery.NewMock<ISyncItemTransfer>();

            using (this.Mockery.Ordered)
            {
                Expect.Once.On(syncServiceA).Method("Connect");
                Expect.Once.On(syncServiceA).Method("EnumerateMediaFiles")
                    .Will(Return.Value(new List<MediaItemMetadata>()));

                Expect.Once.On(syncServiceB).Method("Connect");
                Expect.Once.On(syncServiceB).Method("EnumerateMediaFiles")
                    .Will(Return.Value(items));

                Expect.Once.On(syncServiceB).Method("DeleteMediaFile")
                    // FIXME: .With(/*correct method parameters*/);
                    .WithAnyArguments();

                Expect.Once.On(syncServiceB).Method("Disconnect");
                Expect.Once.On(syncServiceA).Method("Disconnect");
            }

            syncProviderA = new Obra.Sync.SyncProvider(
                syncServiceA,
                this.syncMetadataFileNameA);

            syncProviderB = new Obra.Sync.SyncProvider(
                syncServiceB,
                this.syncMetadataFileNameB);

            using (Orchestrator agent = new Orchestrator(
                syncProviderA,
                syncProviderB))
            {
                agent.Upload();
                this.AssertAgentStatisticsUploadChangesApplied(agent, 1);
            }

            this.AssertNoSynchronizationChanges(
                new List<MediaItemMetadata>());

            // resurrect nameA

            MediaItemMetadata resurrectedItemA = new MediaItemMetadata()
            {
                Name = "nameA",
                Checksum = "resurrected-checksumA",
                LastModifiedUtc = 0,
            };

            List<MediaItemMetadata> resurrectedItems = new List<MediaItemMetadata>()
            {
                resurrectedItemA,
            };

            syncServiceA = this.Mockery.NewMock<ISyncService>();
            syncServiceB = this.Mockery.NewMock<ISyncService>();

            syncItemTransferA = this.Mockery.NewMock<ISyncItemTransfer>();

            using (this.Mockery.Ordered)
            {
                Expect.Once.On(syncServiceA).Method("Connect");
                Expect.Once.On(syncServiceA).Method("EnumerateMediaFiles")
                    .Will(Return.Value(resurrectedItems));

                Expect.Once.On(syncServiceA).Method("ComputeChecksum")
                    .With(resurrectedItemA)
                    .Will(Return.Value(resurrectedItemA.Checksum));

                Expect.Once.On(syncServiceB).Method("Connect");
                Expect.Once.On(syncServiceB).Method("EnumerateMediaFiles")
                    .Will(Return.Value(new List<MediaItemMetadata>()));

                Expect.Once.On(syncServiceA).Method("LoadChange")
                    .With(itemA.Name)
                    .Will(Return.Value(syncItemTransferA));

                Expect.Once.On(syncServiceB).Method("SaveChange")
                    .WithAnyArguments();

                Expect.Once.On(syncItemTransferA).GetProperty("MediaItemMetadata")
                    .Will(Return.Value(resurrectedItemA));

                Expect.Once.On(syncServiceB).Method("Disconnect");
                Expect.Once.On(syncServiceA).Method("Disconnect");
            }

            syncProviderA = new Obra.Sync.SyncProvider(
                syncServiceA,
                this.syncMetadataFileNameA);

            syncProviderB = new Obra.Sync.SyncProvider(
                syncServiceB,
                this.syncMetadataFileNameB);

            using (Orchestrator agent = new Orchestrator(
                syncProviderA,
                syncProviderB))
            {
                agent.Upload();
                this.AssertAgentStatisticsUploadChangesApplied(agent, items.Count);
            }

            this.AssertNoSynchronizationChanges(
                resurrectedItems);
        }
    }
}
